package com.w3c.pragmatic.utils;

/**
 * 雪花算法(64bit组成部分如下)
 *
 * <p>1、 1bit固定位(0-正，1-负，默认:0)
 *
 * <p>2、 41bit时间戳(存储毫秒级时间戳，41bit的长度可以使用69年)
 *
 * <p>3、 10bit标识位(存储机器码，workerIdBits和datacenterIdBits各5bit(可以调整), 两个标识位组合起来最多可以支持部署 1024 个节点)
 *
 * <p>4、 12bit序列号(表示在同一毫秒内生成的多个ID的序号。如果在同一毫秒内生成的ID超过了4096个（2的12次方），则需要等到下一毫秒再生成ID)
 *
 * @author wangdong
 * @since 2025/3/12 9:47
 */
public class SnowflakeUtil {

  private final long twEpoch = 1288834974657L; // 设置一个时间初始值(一般为项目的初始时间)
  private final long workerIdBits = 5L; // 机器ID所占的位数
  private final long datacenterIdBits = 5L; // 数据标识ID所占的位数
  private final long maxWorkerId = ~(-1L << workerIdBits); // 支持的最大机器ID，结果是31
  private final long maxDatacenterId = ~(-1L << datacenterIdBits); // 支持的最大数据标识ID，结果是31
  private final long sequenceBits = 12L; // 序列号所占的位数
  private final long workerIdShift = sequenceBits; // 机器ID向左移12位
  private final long datacenterIdShift = sequenceBits + workerIdBits; // 数据标识ID向左移17位(12+5)
  private final long timestampLeftShift =
      sequenceBits + workerIdBits + datacenterIdBits; // 时间戳向左移22位(12+5+5)
  private final long sequenceMask = ~(-1L << sequenceBits); // 生成序列的掩码，这里是4095

  // 机器ID
  private long workerId;
  // 数据标识ID
  private long datacenterId;
  // 序列号
  private long sequence = 0L;
  // 上次生成ID的时间戳
  private long lastTimestamp = -1L;

  /**
   * 构造器(用于初始化当前服务的机器ID和数据标识ID)
   *
   * @param workerId 机器ID
   * @param datacenterId 数据标识ID
   */
  public SnowflakeUtil(long workerId, long datacenterId) {
    if (workerId > maxWorkerId || workerId < 0) {
      throw new IllegalArgumentException(
          String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
    }
    if (datacenterId > maxDatacenterId || datacenterId < 0) {
      throw new IllegalArgumentException(
          String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
    }
    this.workerId = workerId;
    this.datacenterId = datacenterId;
  }

  /**
   * 获取下一个雪花id
   *
   * @return 64bit转成19位的十进制id
   */
  public synchronized long nextId() {
    long timestamp = timeGen();

    // 这里发生了时钟回拨
    // 方案一：定义一个全局的初始时间戳，每次自己维护，而不依赖系统时间戳
    // 方案二：等待时钟矫正
    if (timestamp < lastTimestamp) {
      // 直接报错
      /*throw new RuntimeException(
      String.format(
          "Clock moved backwards. Refusing to generate id for %d milliseconds",
          lastTimestamp - timestamp));*/
      // 时钟回拨，等待时间追上（这里接受的容忍度是5ms，如果等待时间太长会影响线上业务性能）
      long offset = lastTimestamp - timestamp;
      if (offset <= 5) {
        try {
          wait(offset << 1);
          timestamp = timeGen();
          if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format(
                    "Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
          }
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
      } else {
        throw new RuntimeException(
            String.format(
                "Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
      }
    }
    // 上次生成序列号的时间戳
    // case1、如果时间戳变了表示新的时间戳，则序列号重置为0
    // case2、如果时间戳没变，序列号从4095变成了0（表示被迫等待下个时间戳）
    if (lastTimestamp == timestamp) {
      // 序列号自增
      sequence = (sequence + 1) & sequenceMask;
      // 序列号超过最大值，则等待下一个时间戳
      if (sequence == 0) {
        timestamp = tilNextMillis(lastTimestamp);
      }
    } else {
      // 新的时间戳，序列号重置为0
      sequence = 0L;
    }
    // 用“有效时间戳”生成雪花id
    lastTimestamp = timestamp;
    return ((timestamp - twEpoch) << timestampLeftShift) // 时间戳部分
        | (datacenterId << datacenterIdShift) // 数据中心部分
        | (workerId << workerIdShift) // 机器标识部分
        | sequence; // 序列号部分
  }

  /**
   * 当前时间(毫秒)生成的12bit机器码(4096个)用完了这里获取下个毫秒级时间戳
   *
   * @param lastTimestamp 当前时间戳
   * @return 下移毫秒时间戳
   */
  private long tilNextMillis(long lastTimestamp) {
    long timestamp = timeGen();
    while (timestamp <= lastTimestamp) {
      timestamp = timeGen();
    }
    return timestamp;
  }

  /**
   * 生成当前时间戳(毫秒)
   *
   * @return 精度为ms的时间戳
   */
  private long timeGen() {
    return System.currentTimeMillis();
  }
}
